﻿// Copyright (C) 2012 Jeff Tanner <jeff00seattle@gmail.com>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Globalization;
using System.Linq;

namespace WindowsAzure_StorageREST
{
    public class StorageRequest
    {
        private StorageEndpoint storageEndpoint = null;
        private HttpWebRequest request = null;
        private byte[] content = null;
        private Dictionary<string, string> functionHeaders = null;

        public StorageRequest(StorageEndpoint storageEndpoint)
        {
            this.storageEndpoint = storageEndpoint;
        }

        /// <summary>
        /// Initializes the request.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="queryParams">The query params.</param>
        public void InitializeRequest(string path, Dictionary<string, string> queryParams)
        {
            this.request = (HttpWebRequest)WebRequest.Create(GetUri(path, queryParams));
            this.content = null;
            this.functionHeaders = null;
        }

        public void SetContent(byte[] content)
        {
            this.content = content;
        }

        private void SetDateHeader()
        {
            this.request.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture));
        }

        public void SetFunctionHeaders(Dictionary<string, string> headers)
        {
            this.functionHeaders = headers;
        }

        public void SetMethod(string method)
        {
            this.request.Method = method;
        }

        private void SetContentLength()
        {
            if (this.content == null)
            {
                this.request.ContentLength = 0;
            }
            else
            {
                this.request.ContentLength = this.content.Length;
            }
        }

        private void SetBodyContent(byte[] blobContents)
        {
            this.request.GetRequestStream().Write(blobContents, 0, blobContents.Length);
        }

        private string GetUri(string path, Dictionary<string, string> queryParams)
        {
            StringBuilder uri = new StringBuilder();

            uri.Append(this.storageEndpoint.GetConnectionURI());
            if (path != null)
            {
                uri.AppendFormat("{0}", path);
            }

            if (queryParams != null && queryParams.Count > 0)
            {
                uri.Append("?");
                for (int i = 0; i < queryParams.Count; i++)
                {
                    uri.AppendFormat("{0}={1}", queryParams.ElementAt(i).Key, queryParams.ElementAt(i).Value);
                    if (i < queryParams.Count - 1)
                    {
                        uri.Append("&");
                    }
                }
            }

            return uri.ToString();
        }

        public StorageResponse DispatchRequest()
        {
            try
            {
                SetContentLength();
                SetDateHeader();
                SetFunctionHeaders();
                SetAuthorizationHeader();
                if (this.content != null)
                {
                    SetBodyContent(this.content);
                }

                using (HttpWebResponse response = (HttpWebResponse)this.request.GetResponse())
                {
                    return new StorageResponse(response);
                }
            }
            catch (WebException ex)
            {
                using (HttpWebResponse errorResponse = (HttpWebResponse)ex.Response)
                {
                    return new StorageResponse(errorResponse);
                }
            }
        }

        private void SetFunctionHeaders()
        {
            if (this.functionHeaders != null)
            {
                foreach (KeyValuePair<string, string> header in this.functionHeaders)
                {
                    this.request.Headers.Add(header.Key, header.Value);
                }
            }
        }

        private void SetAuthorizationHeader()
        {
            // Now sign the request
            // For a blob, you need to use this Canonical form:
            //  VERB + "\n" +
            //  Content - MD5 + "\n" +
            //  Content - Type + "\n" +
            //  Date + "\n" +
            //  CanonicalizedHeaders +
            //  CanonicalizedResource;

            StringBuilder signature = new StringBuilder();

            // Verb
            signature.Append(String.Format("{0}{1}", this.request.Method, "\n"));

            // Content-MD5 Header
            signature.Append("\n");

            // Content-Type Header
            signature.Append("\n");

            // Then Date, if we have already added the x-ms-date header, leave this null
            signature.Append("\n");

            // Now for CanonicalizedHeaders
            // TODO: Replace with LINQ statement
            foreach (string header in this.request.Headers)
            {
                if (header.StartsWith("x-ms"))
                {
                    signature.Append(String.Format("{0}:{1}\n", header, this.request.Headers[header]));
                }
            }

            // Now for CanonicalizedResource
            // Format is /{0}/{1} where 0 is name of the account and 1 is resources URI path
            // Also make sure any comp query params are included
            signature.Append(String.Format("/{0}{1}", this.storageEndpoint.Account, GetCanonicalizedResourceURI()));

            // Next, we need to encode our signature using the HMAC-SHA256 algorithm
            byte[] signatureByteForm = System.Text.Encoding.UTF8.GetBytes(signature.ToString());

            System.Security.Cryptography.HMACSHA256 hasher = new System.Security.Cryptography.HMACSHA256(Convert.FromBase64String(this.storageEndpoint.Key));

            // Now build the Authorization header
            String authHeader = String.Format(CultureInfo.InvariantCulture,
                                       "{0} {1}:{2}",
                                       "SharedKey",
                                       this.storageEndpoint.Account,
                                       System.Convert.ToBase64String(hasher.ComputeHash(signatureByteForm)
                                       ));

            // And add the Authorization header to the request
            this.request.Headers.Add("Authorization", authHeader);
        }

        private string GetCanonicalizedResourceURI()
        {
            StringBuilder sb = new StringBuilder();

            sb.Append(this.request.RequestUri.AbsolutePath);

            // If there are any comp query params, they needed to be added, but not anything else
            if (this.request.RequestUri.Query.Contains("comp"))
            {
                sb.Append(this.request.RequestUri.Query);
            }

            return sb.ToString();
        }
    }
}
